home *** CD-ROM | disk | FTP | other *** search
/ PC World Komputer 2010 April / PCWorld0410.iso / hity wydania / Ubuntu 9.10 PL / karmelkowy-koliberek-desktop-9.10-i386-PL.iso / casper / filesystem.squashfs / usr / share / system-config-printer / jobviewer.py < prev    next >
Text File  |  2009-10-19  |  64KB  |  1,646 lines

  1.  
  2. ## Copyright (C) 2007, 2008, 2009 Tim Waugh <twaugh@redhat.com>
  3. ## Copyright (C) 2007, 2008, 2009 Red Hat, Inc.
  4.  
  5. ## This program is free software; you can redistribute it and/or modify
  6. ## it under the terms of the GNU General Public License as published by
  7. ## the Free Software Foundation; either version 2 of the License, or
  8. ## (at your option) any later version.
  9.  
  10. ## This program is distributed in the hope that it will be useful,
  11. ## but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  13. ## GNU General Public License for more details.
  14.  
  15. ## You should have received a copy of the GNU General Public License
  16. ## along with this program; if not, write to the Free Software
  17. ## Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
  18.  
  19. import authconn
  20. import cups
  21. import dbus
  22. import dbus.glib
  23. import dbus.service
  24. import pynotify
  25. import gettext
  26. import gobject
  27. import gtk
  28. import gtk.gdk
  29. import gtk.glade
  30. from glade import GtkGUI
  31. import monitor
  32. import os
  33. import pango
  34. import pwd
  35. import smburi
  36. import subprocess
  37. import sys
  38. import time
  39. import urllib
  40.  
  41. from debug import *
  42. import config
  43. import statereason
  44. import errordialogs
  45. import pprint
  46.  
  47. try:
  48.     import gnomekeyring
  49.     USE_KEYRING=True
  50. except ImportError:
  51.     USE_KEYRING=False
  52.  
  53. from gettext import gettext as _
  54. DOMAIN="system-config-printer"
  55. gettext.textdomain (DOMAIN)
  56. gtk.glade.textdomain (DOMAIN)
  57. gtk.glade.bindtextdomain (DOMAIN)
  58. from statereason import StateReason
  59. statereason.set_gettext_function (_)
  60. errordialogs.set_gettext_function (_)
  61.  
  62. pkgdata = config.pkgdatadir
  63. GLADE="applet.glade"
  64. ICON="printer"
  65. SEARCHING_ICON="document-print-preview"
  66.  
  67. # We need to call pynotify.init before we can check the server for caps
  68. pynotify.init('System Config Printer Notification')
  69.  
  70. class PrinterURIIndex:
  71.     def __init__ (self, names=None):
  72.         self.printer = {}
  73.         self.names = names
  74.  
  75.     def update_from_attrs (self, printer, attrs):
  76.         uris = []
  77.         if attrs.has_key ('printer-uri-supported'):
  78.             uri_supported = attrs['printer-uri-supported']
  79.             if type (uri_supported) != list:
  80.                 uri_supported = [uri_supported]
  81.             uris.extend (uri_supported)
  82.         if attrs.has_key ('notify-printer-uri'):
  83.             uris.append (attrs['notify-printer-uri'])
  84.         if attrs.has_key ('printer-more-info'):
  85.             uris.append (attrs['printer-more-info'])
  86.  
  87.         for uri in uris:
  88.             self.printer[uri] = printer
  89.  
  90.     def remove_printer (self, printer):
  91.         # Remove references to this printer in the URI map.
  92.         uris = self.printer.keys ()
  93.         for uri in uris:
  94.             if self.printer[uri] == printer:
  95.                 del self.printer[uri]
  96.  
  97.     def lookup (self, uri, connection=None):
  98.         try:
  99.             return self.printer[uri]
  100.         except KeyError:
  101.             if connection == None:
  102.                 connection = cups.Connection ()
  103.  
  104.             r = ['printer-name', 'printer-uri-supported', 'printer-more-info']
  105.             try:
  106.                 attrs = connection.getPrinterAttributes (uri=uri,
  107.                                                          requested_attributes=r)
  108.             except cups.IPPError:
  109.                 # URI not known.
  110.                 raise KeyError
  111.  
  112.             name = attrs['printer-name']
  113.             self.update_from_attrs (name, attrs)
  114.             self.printer[uri] = name
  115.             try:
  116.                 return self.printer[uri]
  117.             except KeyError:
  118.                 pass
  119.         raise KeyError
  120.  
  121.  
  122. class JobViewer (GtkGUI, monitor.Watcher):
  123.     required_job_attributes = set(['job-k-octets',
  124.                                    'job-name',
  125.                                    'job-originating-user-name',
  126.                                    'job-printer-uri',
  127.                                    'job-state',
  128.                                    'time-at-creation'])
  129.  
  130.     def __init__(self, bus=None, loop=None, service_running=False,
  131.                  trayicon=False, suppress_icon_hide=False,
  132.                  my_jobs=True, specific_dests=None, exit_handler=None,
  133.                  parent=None):
  134.         self.loop = loop
  135.         self.service_running = service_running
  136.         self.trayicon = trayicon
  137.         self.suppress_icon_hide = suppress_icon_hide
  138.         self.my_jobs = my_jobs
  139.         self.specific_dests = specific_dests
  140.         self.exit_handler = exit_handler
  141.  
  142.         self.jobs = {}
  143.         self.jobiters = {}
  144.         self.active_jobs = set() # of job IDs
  145.         self.stopped_job_prompts = set() # of job IDs
  146.         self.printer_state_reasons = {}
  147.         self.num_jobs_when_hidden = 0
  148.         self.connecting_to_device = {} # dict of printer->time first seen
  149.         self.state_reason_notifications = {}
  150.         self.auth_info_dialogs = {} # by job ID
  151.         self.job_creation_times_timer = None
  152.         self.special_status_icon = False
  153.         self.new_printer_notifications = {}
  154.         self.authenticated_jobs = set() # of job IDs
  155.  
  156.         self.getWidgets ({"JobsWindow":
  157.                               ["JobsWindow",
  158.                                "job_menubar_item",
  159.                                "treeview",
  160.                                "statusbar"],
  161.                           "statusicon_popupmenu":
  162.                               ["statusicon_popupmenu"]})
  163.  
  164.         job_action_group = gtk.ActionGroup ("JobActionGroup")
  165.         job_action_group.add_actions ([
  166.                 ("cancel-job", gtk.STOCK_CANCEL, None, None, None,
  167.                  self.on_job_cancel_activate),
  168.                 ("hold-job", gtk.STOCK_MEDIA_PAUSE, _("_Hold"), None, None,
  169.                  self.on_job_hold_activate),
  170.                 ("release-job", gtk.STOCK_MEDIA_PLAY, _("_Release"), None, None,
  171.                  self.on_job_release_activate),
  172.                 ("reprint-job", gtk.STOCK_REDO, _("Re_print"), None, None,
  173.                  self.on_job_reprint_activate),
  174.                 ("authenticate-job", None, _("_Authenticate"), None, None,
  175.                  self.on_job_authenticate_activate)
  176.                 ])
  177.         self.job_ui_manager = gtk.UIManager ()
  178.         self.job_ui_manager.insert_action_group (job_action_group, -1)
  179.         self.job_ui_manager.add_ui_from_string (
  180. """
  181. <ui>
  182.  <accelerator action="cancel-job"/>
  183.  <accelerator action="hold-job"/>
  184.  <accelerator action="release-job"/>
  185.  <accelerator action="reprint-job"/>
  186.  <accelerator action="authenticate-job"/>
  187. </ui>
  188. """
  189. )
  190.         self.job_ui_manager.ensure_update ()
  191.         self.JobsWindow.add_accel_group (self.job_ui_manager.get_accel_group ())
  192.         self.job_context_menu = gtk.Menu ()
  193.         for action_name in ["cancel-job",
  194.                             "hold-job",
  195.                             "release-job",
  196.                             "reprint-job",
  197.                             None,
  198.                             "authenticate-job"]:
  199.             if not action_name:
  200.                 item = gtk.SeparatorMenuItem ()
  201.             else:
  202.                 action = job_action_group.get_action (action_name)
  203.                 action.set_sensitive (False)
  204.                 item = action.create_menu_item ()
  205.  
  206.             item.show ()
  207.             self.job_context_menu.append (item)
  208.  
  209.         self.job_menubar_item.set_submenu (self.job_context_menu)
  210.  
  211.         for skip, ellipsize, name, setter in \
  212.                 [(False, False, _("Job"), self._set_job_job_number_text),
  213.                  (True, False, _("User"), self._set_job_user_text),
  214.                  (False, True, _("Document"), self._set_job_document_text),
  215.                  (False, True, _("Printer"), self._set_job_printer_text),
  216.                  (False, False, _("Size"), self._set_job_size_text)]:
  217.             if trayicon and skip:
  218.                 # Skip the user column for the trayicon.
  219.                 continue
  220.  
  221.             cell = gtk.CellRendererText()
  222.             if ellipsize:
  223.                 # Ellipsize the 'Document' and 'Printer' columns.
  224.                 cell.set_property ("ellipsize", pango.ELLIPSIZE_END)
  225.                 cell.set_property ("width-chars", 20)
  226.             column = gtk.TreeViewColumn(name, cell)
  227.             column.set_cell_data_func (cell, setter)
  228.             column.set_resizable(True)
  229.             self.treeview.append_column(column)
  230.  
  231.         cell = gtk.CellRendererText ()
  232.         column = gtk.TreeViewColumn (_("Time submitted"), cell, text=1)
  233.         column.set_resizable (True)
  234.         self.treeview.append_column (column)
  235.  
  236.         column = gtk.TreeViewColumn (_("Status"))
  237.         icon = gtk.CellRendererPixbuf ()
  238.         column.pack_start (icon, False)
  239.         text = gtk.CellRendererText ()
  240.         text.set_property ("ellipsize", pango.ELLIPSIZE_END)
  241.         text.set_property ("width-chars", 20)
  242.         column.pack_start (text, True)
  243.         column.set_cell_data_func (icon, self._set_job_status_icon)
  244.         column.set_cell_data_func (text, self._set_job_status_text)
  245.         self.treeview.append_column (column)
  246.  
  247.         self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
  248.         self.store = gtk.TreeStore(int, str)
  249.         self.store.set_sort_column_id (0, gtk.SORT_DESCENDING)
  250.         self.treeview.set_model(self.store)
  251.         self.treeview.set_rules_hint (True)
  252.         self.treeview.connect ('button_release_event',
  253.                                self.on_treeview_button_release_event)
  254.         self.treeview.connect ('popup-menu', self.on_treeview_popup_menu)
  255.         self.treeview.connect ('cursor-changed',
  256.                                self.on_treeview_cursor_changed)
  257.         self.store.connect ('row-changed',
  258.                             self.on_treemodel_row_changed)
  259.  
  260.         self.JobsWindow.set_icon_name (ICON)
  261.         self.JobsWindow.hide ()
  262.  
  263.         if specific_dests:
  264.             the_dests = reduce (lambda x, y: x + ", " + y, specific_dests)
  265.  
  266.         if my_jobs:
  267.             if specific_dests:
  268.                 title = _("my jobs on %s") % the_dests
  269.             else:
  270.                 title = _("my jobs")
  271.         else:
  272.             if specific_dests:
  273.                 title = "%s" % the_dests
  274.             else:
  275.                 title = _("all jobs")
  276.         self.JobsWindow.set_title (_("Document Print Status (%s)") % title)
  277.  
  278.         if parent:
  279.             self.JobsWindow.set_transient_for (parent)
  280.  
  281.         self.statusbar_set = False
  282.  
  283.         theme = gtk.icon_theme_get_default ()
  284.         self.icon_jobs = theme.load_icon (ICON, 22, 0)
  285.         self.icon_jobs_processing = theme.load_icon ("printer-printing",
  286.                                                      22, 0)
  287.         self.icon_no_jobs = self.icon_jobs.copy ()
  288.         self.icon_no_jobs.fill (0)
  289.         self.icon_jobs.composite (self.icon_no_jobs,
  290.                                   0, 0,
  291.                                   self.icon_no_jobs.get_width(),
  292.                                   self.icon_no_jobs.get_height(),
  293.                                   0, 0,
  294.                                   1.0, 1.0,
  295.                                   gtk.gdk.INTERP_BILINEAR,
  296.                                   127)
  297.         if self.trayicon:
  298.             self.statusicon = gtk.StatusIcon ()
  299.             pixbuf = theme.load_icon (ICON, 22, 0)
  300.             self.statusicon.set_from_pixbuf (pixbuf)    
  301.             self.set_statusicon_from_pixbuf (self.icon_no_jobs)
  302.             self.statusicon.connect ('activate', self.toggle_window_display)
  303.             self.statusicon.connect ('popup-menu', self.on_icon_popupmenu)
  304.             self.statusicon.set_visible (False)
  305.  
  306.         # D-Bus
  307.         if bus == None:
  308.             bus = dbus.SystemBus ()
  309.  
  310.         self.set_process_pending (True)
  311.         self.host = cups.getServer ()
  312.         self.port = cups.getPort ()
  313.         self.encryption = cups.getEncryption ()
  314.         self.monitor = monitor.Monitor (self, bus=bus, my_jobs=my_jobs,
  315.                                         specific_dests=specific_dests,
  316.                                         host=self.host, port=self.port,
  317.                                         encryption=self.encryption)
  318.  
  319.         if not self.trayicon:
  320.             self.JobsWindow.show ()
  321.  
  322.     def cleanup (self):
  323.         self.monitor.cleanup ()
  324.  
  325.         # Close any open notifications.
  326.         for l in [self.new_printer_notifications.values (),
  327.                   self.state_reason_notifications.values ()]:
  328.             for notification in l:
  329.                 if notification.get_data ('closed') != True:
  330.                     notification.close ()
  331.                     notification.set_data ('closed', True)
  332.  
  333.         if self.job_creation_times_timer != None:
  334.             gobject.source_remove (self.job_creation_times_timer)
  335.             self.job_creation_times_timer = None
  336.  
  337.         if self.exit_handler:
  338.             self.exit_handler (self)
  339.  
  340.     def set_process_pending (self, whether):
  341.         self.process_pending_events = whether
  342.  
  343.     # Handle "special" status icon
  344.     def set_special_statusicon (self, iconname, tooltip=None):
  345.         self.special_status_icon = True
  346.         self.statusicon.set_from_icon_name (iconname)
  347.         self.set_statusicon_visibility ()
  348.         if tooltip != None:
  349.             self.set_statusicon_tooltip (tooltip=tooltip)
  350.  
  351.     def unset_special_statusicon (self):
  352.         self.special_status_icon = False
  353.         self.statusicon.set_from_pixbuf (self.saved_statusicon_pixbuf)
  354.         self.set_statusicon_visibility ()
  355.         self.set_statusicon_tooltip ()
  356.  
  357.     def notify_new_printer (self, printer, notification):
  358.         self.new_printer_notifications[printer] = notification
  359.         notification.set_data ('printer-name', printer)
  360.         notification.connect ('closed', self.on_new_printer_notification_closed)
  361.         self.set_statusicon_visibility ()
  362.         notification.attach_to_status_icon (self.statusicon)
  363.         try:
  364.             notification.show ()
  365.         except gobject.GError:
  366.             nonfatalException ()
  367.  
  368.     def on_new_printer_notification_closed (self, notification, reason=None):
  369.         printer = notification.get_data ('printer-name')
  370.         del self.new_printer_notifications[printer]
  371.         self.set_statusicon_visibility ()
  372.  
  373.     def set_statusicon_from_pixbuf (self, pb):
  374.         self.saved_statusicon_pixbuf = pb
  375.         if not self.special_status_icon:
  376.             self.statusicon.set_from_pixbuf (pb)
  377.  
  378.     def on_delete_event(self, *args):
  379.         if self.trayicon or not self.loop:
  380.             self.JobsWindow.hide ()
  381.             if not self.loop:
  382.                 # Being run from main app, not applet
  383.                 self.cleanup ()
  384.         else:
  385.             self.loop.quit ()
  386.         return True
  387.  
  388.     def show_IPP_Error(self, exception, message):
  389.         return errordialogs.show_IPP_Error (exception, message, self.JobsWindow)
  390.  
  391.     def toggle_window_display(self, icon, force_show=False):
  392.         visible = self.JobsWindow.get_property('visible')
  393.         if force_show:
  394.             visible = False
  395.  
  396.         if visible:
  397.             self.JobsWindow.hide()
  398.         else:
  399.             self.JobsWindow.show()
  400.  
  401.     def on_show_completed_jobs_activate(self, menuitem):
  402.         if menuitem.get_active():
  403.             which_jobs = "all"
  404.         else:
  405.             which_jobs = "not-completed"
  406.         self.monitor.refresh(which_jobs=which_jobs, refresh_all=False)
  407.  
  408.     def update_job_creation_times(self):
  409.         now = time.time ()
  410.         need_update = False
  411.         for job, data in self.jobs.iteritems():
  412.             if self.jobs.has_key (job):
  413.                 iter = self.jobiters[job]
  414.  
  415.             t = _("Unknown")
  416.             if data.has_key ('time-at-creation'):
  417.                 created = data['time-at-creation']
  418.                 ago = now - created
  419.                 need_update = True
  420.                 if ago < 2 * 60:
  421.                     t = _("a minute ago")
  422.                 elif ago < 60 * 60:
  423.                     mins = int (ago / 60)
  424.                     t = _("%d minutes ago") % mins
  425.                 elif ago < 24 * 60 * 60:
  426.                     hours = int (ago / (60 * 60))
  427.                     if hours == 1:
  428.                         t = _("an hour ago")
  429.                     else:
  430.                         t = _("%d hours ago") % hours
  431.                 elif ago < 7 * 24 * 60 * 60:
  432.                     days = int (ago / (24 * 60 * 60))
  433.                     if days == 1:
  434.                         t = _("yesterday")
  435.                     else:
  436.                         t = _("%d days ago") % days
  437.                 elif ago < 6 * 7 * 24 * 60 * 60:
  438.                     weeks = int (ago / (7 * 24 * 60 * 60))
  439.                     if weeks == 1:
  440.                         t = _("last week")
  441.                     else:
  442.                         t = _("%d weeks ago") % weeks
  443.                 else:
  444.                     need_update = False
  445.                     t = time.strftime ("%B %Y", time.localtime (created))
  446.  
  447.             self.store.set_value (iter, 1, t)
  448.  
  449.         if need_update and not self.job_creation_times_timer:
  450.             t = gobject.timeout_add (60 * 1000, self.update_job_creation_times)
  451.             self.job_creation_times_timer = t
  452.  
  453.         if not need_update:
  454.             if self.job_creation_times_timer:
  455.                 gobject.source_remove (self.job_creation_times_timer)
  456.                 self.job_creation_times_timer = None
  457.  
  458.         # Return code controls whether the timeout will recur.
  459.         return need_update
  460.  
  461.     def print_error_dialog_response(self, dialog, response, jobid):
  462.         dialog.hide ()
  463.         dialog.destroy ()
  464.         self.stopped_job_prompts.remove (jobid)
  465.         if response == gtk.RESPONSE_NO:
  466.             # Diagnose
  467.             if not self.__dict__.has_key ('troubleshooter'):
  468.                 import troubleshoot
  469.                 troubleshooter = troubleshoot.run (self.on_troubleshoot_quit)
  470.                 self.troubleshooter = troubleshooter
  471.  
  472.     def on_troubleshoot_quit(self, troubleshooter):
  473.         del self.troubleshooter
  474.  
  475.     def add_job (self, job, data, connection=None):
  476.         self.update_job (job, data, connection=connection)
  477.  
  478.         store = self.store
  479.         iter = self.store.append (None)
  480.         store.set_value (iter, 0, job)
  481.         debugprint ("Job %d added" % job)
  482.         self.jobiters[job] = iter
  483.  
  484.         range = self.treeview.get_visible_range ()
  485.         if range != None:
  486.             (start, end) = range
  487.             if (self.store.get_sort_column_id () == (0,
  488.                                                      gtk.SORT_DESCENDING) and
  489.                 start == (1,)):
  490.                 # This job was added job above the visible range, and
  491.                 # we are sorting by descending job ID.  Scroll to it.
  492.                 self.treeview.scroll_to_cell ((0,), None, False, 0.0, 0.0)
  493.  
  494.         if not self.job_creation_times_timer:
  495.             def start_updating_job_creation_times():
  496.                 self.update_job_creation_times ()
  497.                 return False
  498.  
  499.             gobject.timeout_add (500, start_updating_job_creation_times)
  500.  
  501.     def update_job (self, job, data, connection=None):
  502.         # Fetch required attributes for this job if they are missing.
  503.         r = self.required_job_attributes - set (data.keys ())
  504.  
  505.         if r:
  506.             attrs = None
  507.             try:
  508.                 if connection == None:
  509.                     connection = cups.Connection (host=self.host,
  510.                                                   port=self.port,
  511.                                                   encryption=self.encryption)
  512.  
  513.                 debugprint ("requesting %s" % r)
  514.                 r = list (r)
  515.                 attrs = connection.getJobAttributes (job,
  516.                                                      requested_attributes=r)
  517.             except RuntimeError:
  518.                 pass
  519.             except AttributeError:
  520.                 pass
  521.  
  522.             if attrs:
  523.                 data.update (attrs)
  524.  
  525.         self.jobs[job] = data
  526.  
  527.         job_requires_auth = False
  528.         try:
  529.             jstate = data.get ('job-state', cups.IPP_JOB_PROCESSING)
  530.             s = int (jstate)
  531.  
  532.             if s in [cups.IPP_JOB_HELD, cups.IPP_JOB_STOPPED]:
  533.                 jattrs = ['job-state', 'job-hold-until']
  534.                 pattrs = ['auth-info-required', 'device-uri']
  535.                 uri = data.get ('job-printer-uri')
  536.                 c = authconn.Connection (self.JobsWindow,
  537.                                          host=self.host,
  538.                                          port=self.port,
  539.                                          encryption=self.encryption)
  540.                 attrs = c.getPrinterAttributes (uri = uri,
  541.                                                 requested_attributes=pattrs)
  542.  
  543.                 try:
  544.                     auth_info_required = attrs['auth-info-required']
  545.                 except KeyError:
  546.                     debugprint ("No auth-info-required attribute; "
  547.                                 "guessing instead")
  548.                     auth_info_required = ['username', 'password']
  549.  
  550.                 if not isinstance (auth_info_required, list):
  551.                     auth_info_required = [auth_info_required]
  552.                     attrs['auth-info-required'] = auth_info_required
  553.  
  554.                 data.update (attrs)
  555.  
  556.                 attrs = c.getJobAttributes (job,
  557.                                             requested_attributes=jattrs)
  558.                 data.update (attrs)
  559.                 jstate = data.get ('job-state', cups.IPP_JOB_PROCESSING)
  560.                 s = int (jstate)
  561.         except ValueError:
  562.             pass
  563.         except RuntimeError:
  564.             pass
  565.         except cups.IPPError, (e, m):
  566.             pass
  567.  
  568.         # Invalidate the cached status description and redraw the treeview.
  569.         try:
  570.             del data['_status_text']
  571.         except KeyError:
  572.             pass
  573.         self.treeview.queue_draw ()
  574.  
  575.         # Check whether authentication is required.
  576.         if self.trayicon:
  577.             job_requires_auth = (s == cups.IPP_JOB_HELD and
  578.                                  data.get ('job-hold-until', 'none') ==
  579.                                  'auth-info-required')
  580.  
  581.             if (job_requires_auth and
  582.                 not self.auth_info_dialogs.has_key (job)):
  583.                 try:
  584.                     cups.require ("1.9.37")
  585.                 except:
  586.                     debugprint ("Authentication required but "
  587.                                 "authenticateJob() not available")
  588.                     return
  589.  
  590.                 # Find out which auth-info is required.
  591.                 try_keyring = USE_KEYRING
  592.                 keyring_attrs = None
  593.                 auth_info = None
  594.                 if try_keyring and 'password' in auth_info_required:
  595.                     auth_info_required = data.get ('auth-info-required', [])
  596.                     device_uri = data.get ("device-uri")
  597.                     (scheme, rest) = urllib.splittype (device_uri)
  598.                     keyring_attrs = dict ()
  599.                     if scheme == 'smb':
  600.                         uri = smburi.SMBURI (uri=device_uri)
  601.                         (group, server, share,
  602.                          user, password) = uri.separate ()
  603.                         keyring_attrs["domain"] = str (group)
  604.                     else:
  605.                         (serverport, rest) = urllib.splithost (rest)
  606.                         (server, port) = urllib.splitnport (serverport)
  607.                     username = pwd.getpwuid (os.getuid ())[0]
  608.                     keyring_attrs.update ({ "server": str (server.lower ()),
  609.                                             "protocol": str (scheme),
  610.                                             "user": str (username)})
  611.  
  612.                 if job in self.authenticated_jobs:
  613.                     # We've already tried to authenticate this job before.
  614.                     try_keyring = False
  615.  
  616.                 if try_keyring and 'password' in auth_info_required:
  617.                     type = gnomekeyring.ITEM_NETWORK_PASSWORD
  618.                     try:
  619.                         items = gnomekeyring.find_items_sync (type,
  620.                                                               keyring_attrs)
  621.                         auth_info = map (lambda x: '', auth_info_required)
  622.                         ind = auth_info_required.index ('username')
  623.                         auth_info[ind] = items[0].attributes.get ('user', '')
  624.                         ind = auth_info_required.index ('password')
  625.                         auth_info[ind] = items[0].secret
  626.                     except gnomekeyring.NoMatchError:
  627.                         debugprint ("gnomekeyring: no match for %s" %
  628.                                     keyring_attrs)
  629.                     except gnomekeyring.DeniedError:
  630.                         debugprint ("gnomekeyring: denied for %s" %
  631.                                     keyring_attrs)
  632.  
  633.                 if try_keyring and c == None:
  634.                     try:
  635.                         c = authconn.Connection (self.JobsWindow,
  636.                                                  host=self.host,
  637.                                                  port=self.port,
  638.                                                  encryption=self.encryption)
  639.                     except RuntimeError:
  640.                         try_keyring = False
  641.  
  642.                 if try_keyring and auth_info != None:
  643.                     try:
  644.                         c._begin_operation (_("authenticating job"))
  645.                         c.authenticateJob (job, auth_info)
  646.                         c._end_operation ()
  647.                         self.monitor.update ()
  648.                         debugprint ("Automatically authenticated job %d" % job)
  649.                         self.authenticated_jobs.add (job)
  650.                         return
  651.                     except cups.IPPError, (e, m):
  652.                         c._end_operation ()
  653.                         nonfatalException ()
  654.                         return
  655.                     except:
  656.                         c._end_operation ()
  657.                         nonfatalException ()
  658.  
  659.                 self.display_auth_info_dialog (job)
  660.  
  661.     def on_auth_notification_closed (self, notification, reason=None):
  662.         job = notification.get_data ('job-id')
  663.         debugprint ("auth notification closed for job %s" % job)
  664.         self.auth_notifications[job].set_data ('closed', True)
  665.         del self.auth_notifications[job]
  666.  
  667.     def on_auth_notification_authenticate (self, notification, action):
  668.         job = notification.get_data ('job-id')
  669.         keyring_attrs = notification.get_data ('keyring-attrs')
  670.         debugprint ("auth notification authenticate for job %s" % job)
  671.         self.display_auth_info_dialog (job, keyring_attrs)
  672.  
  673.     def display_auth_info_dialog (self, job, keyring_attrs=None):
  674.         data = self.jobs[job]
  675.         auth_info_required = data['auth-info-required']
  676.         dialog = authconn.AuthDialog (auth_info_required=auth_info_required,
  677.                                       allow_remember=USE_KEYRING)
  678.         dialog.set_data ('keyring-attrs', keyring_attrs)
  679.         dialog.set_data ('auth-info-required', auth_info_required)
  680.         dialog.set_position (gtk.WIN_POS_CENTER)
  681.  
  682.         # Pre-fill 'username' field.
  683.         auth_info = map (lambda x: '', auth_info_required)
  684.         username = pwd.getpwuid (os.getuid ())[0]
  685.         if 'username' in auth_info_required:
  686.             try:
  687.                 ind = auth_info_required.index ('username')
  688.                 auth_info[ind] = username
  689.                 dialog.set_auth_info (auth_info)
  690.             except:
  691.                 nonfatalException ()
  692.  
  693.         # Focus on the first empty field.
  694.         index = 0
  695.         for field in auth_info_required:
  696.             if auth_info[index] == '':
  697.                 dialog.field_grab_focus (field)
  698.                 break
  699.             index += 1
  700.  
  701.         dialog.set_prompt (_("Authentication required for "
  702.                              "printing document `%s' (job %d)") %
  703.                            (data.get('job-name', _("Unknown")), job))
  704.         self.auth_info_dialogs[job] = dialog
  705.         dialog.connect ('response', self.auth_info_dialog_response)
  706.         dialog.connect ('delete-event', self.auth_info_dialog_delete)
  707.         dialog.set_data ('job-id', job)
  708.         dialog.show_all ()
  709.         dialog.set_keep_above (True)
  710.         dialog.show_now ()
  711.  
  712.     def auth_info_dialog_delete (self, dialog, event):
  713.         self.auth_info_dialog_response (dialog, gtk.RESPONSE_CANCEL)
  714.  
  715.     def auth_info_dialog_response (self, dialog, response):
  716.         jobid = dialog.get_data ('job-id')
  717.         del self.auth_info_dialogs[jobid]
  718.         if response != gtk.RESPONSE_OK:
  719.             dialog.destroy ()
  720.             return
  721.  
  722.         auth_info = dialog.get_auth_info ()
  723.         try:
  724.             c = authconn.Connection (self.JobsWindow,
  725.                                      host=self.host,
  726.                                      port=self.port,
  727.                                      encryption=self.encryption)
  728.         except RuntimeError:
  729.             debugprint ("Error connecting to CUPS for authentication")
  730.             return
  731.  
  732.         remember = False
  733.         c._begin_operation (_("authenticating job"))
  734.         try:
  735.             c.authenticateJob (jobid, auth_info)
  736.             remember = dialog.get_remember_password ()
  737.             self.authenticated_jobs.add (jobid)
  738.             self.monitor.update ()
  739.         except cups.IPPError, (e, m):
  740.             self.show_IPP_Error (e, m)
  741.  
  742.         c._end_operation ()
  743.  
  744.         if remember:
  745.             try:
  746.                 keyring = gnomekeyring.get_default_keyring_sync ()
  747.                 type = gnomekeyring.ITEM_NETWORK_PASSWORD
  748.                 attrs = dialog.get_data ("keyring-attrs")
  749.                 auth_info_required = dialog.get_data ('auth-info-required')
  750.                 if attrs != None and auth_info_required != None:
  751.                     try:
  752.                         ind = auth_info_required.index ('username')
  753.                         attrs['user'] = auth_info[ind]
  754.                     except IndexError:
  755.                         pass
  756.  
  757.                     name = "%s@%s (%s)" % (attrs.get ("user"),
  758.                                            attrs.get ("server"),
  759.                                            attrs.get ("protocol"))
  760.                     ind = auth_info_required.index ('password')
  761.                     secret = auth_info[ind]
  762.                     gnomekeyring.item_create_sync (keyring, type, name,
  763.                                                    attrs, secret, True)
  764.             except:
  765.                 nonfatalException ()
  766.  
  767.         dialog.destroy ()
  768.  
  769.     def set_statusicon_visibility (self):
  770.         if not self.trayicon:
  771.             return
  772.  
  773.         if self.suppress_icon_hide:
  774.             # Avoid hiding the icon if we've been woken up to notify
  775.             # about a new printer.
  776.             self.suppress_icon_hide = False
  777.             return
  778.  
  779.         open_notifications = len (self.new_printer_notifications.keys ())
  780.         for reason, notification in self.state_reason_notifications.iteritems():
  781.             if notification.get_data ('closed') != True:
  782.                 open_notifications += 1
  783.         num_jobs = len (self.active_jobs)
  784.  
  785.         debugprint ("open notifications: %d" % open_notifications)
  786.         debugprint ("num_jobs: %d" % num_jobs)
  787.         debugprint ("num_jobs_when_hidden: %d" % self.num_jobs_when_hidden)
  788.  
  789.         self.statusicon.set_visible (self.special_status_icon or
  790.                                      open_notifications > 0 or
  791.                                      num_jobs > self.num_jobs_when_hidden)
  792.  
  793.         # Let the icon show/hide itself before continuing.
  794.         while self.process_pending_events and gtk.events_pending ():
  795.             gtk.main_iteration ()
  796.  
  797.     def on_treeview_popup_menu (self, treeview):
  798.         event = gtk.gdk.Event (gtk.gdk.NOTHING)
  799.         self.show_treeview_popup_menu (treeview, event, 0)
  800.  
  801.     def on_treeview_button_release_event(self, treeview, event):
  802.         if event.button == 3:
  803.             self.show_treeview_popup_menu (treeview, event, event.button)
  804.  
  805.     def on_treemodel_row_changed (self, model, path, iter):
  806.         self.on_treeview_cursor_changed (self.treeview)
  807.  
  808.     def on_treeview_cursor_changed (self, treeview):
  809.         path, column = treeview.get_cursor ()
  810.         cancel = self.job_ui_manager.get_action ("/cancel-job")
  811.         hold = self.job_ui_manager.get_action ("/hold-job")
  812.         release = self.job_ui_manager.get_action ("/release-job")
  813.         reprint = self.job_ui_manager.get_action ("/reprint-job")
  814.         authenticate = self.job_ui_manager.get_action ("/authenticate-job")
  815.         if path == None:
  816.             for widget in [cancel, hold, release, reprint, authenticate]:
  817.                 widget.set_sensitive (False)
  818.             return
  819.  
  820.         iter = self.store.get_iter (path)
  821.         self.jobid = self.store.get_value (iter, 0)
  822.         job = self.jobs[self.jobid]
  823.         authenticate.set_sensitive (False)
  824.         for widget in [cancel, hold, release, reprint]:
  825.             widget.set_sensitive (True)
  826.  
  827.         if job.has_key ('job-state'):
  828.             s = job['job-state']
  829.             if s >= cups.IPP_JOB_CANCELED:
  830.                 cancel.set_sensitive (False)
  831.             if s != cups.IPP_JOB_PENDING:
  832.                 hold.set_sensitive (False)
  833.             if s != cups.IPP_JOB_HELD:
  834.                 release.set_sensitive (False)
  835.             if (not job.get('job-preserved', False)):
  836.                 reprint.set_sensitive (False)
  837.  
  838.         if job.get ('job-state', cups.IPP_JOB_CANCELED) == cups.IPP_JOB_HELD:
  839.             if job.get ('job-hold-until', 'none') == 'auth-info-required':
  840.                 authenticate.set_sensitive (True)
  841.  
  842.     def show_treeview_popup_menu (self, treeview, event, event_button):
  843.         # Right-clicked.
  844.         self.job_context_menu.popup (None, None, None, event_button,
  845.                                      event.get_time ())
  846.  
  847.     def on_icon_popupmenu(self, icon, button, time):
  848.         self.statusicon_popupmenu.popup (None, None, None, button, time)
  849.  
  850.     def on_icon_hide_activate(self, menuitem):
  851.         self.num_jobs_when_hidden = len (self.jobs.keys ())
  852.         self.set_statusicon_visibility ()
  853.  
  854.     def on_icon_configure_printers_activate(self, menuitem):
  855.         if self.loop:
  856.             env = {}
  857.             for name, value in os.environ.iteritems ():
  858.                 if name == "SYSTEM_CONFIG_PRINTER_GLADE":
  859.                     continue
  860.                 env[name] = value
  861.             p = subprocess.Popen ([ "system-config-printer" ],
  862.                                   close_fds=True, env=env)
  863.             gobject.timeout_add (10 * 1000, self.poll_subprocess, p)
  864.  
  865.     def poll_subprocess(self, process):
  866.         returncode = process.poll ()
  867.         return returncode == None
  868.  
  869.     def on_icon_quit_activate (self, menuitem):
  870.         if self.loop:
  871.             self.loop.quit ()
  872.  
  873.     def on_job_cancel_activate(self, menuitem):
  874.         dialog = gtk.Dialog (_("Cancel Job"), self.JobsWindow,
  875.                              gtk.DIALOG_MODAL |
  876.                              gtk.DIALOG_DESTROY_WITH_PARENT |
  877.                              gtk.DIALOG_NO_SEPARATOR,
  878.                              (gtk.STOCK_NO, gtk.RESPONSE_NO,
  879.                               gtk.STOCK_YES, gtk.RESPONSE_YES))
  880.         dialog.set_default_response (gtk.RESPONSE_NO)
  881.         dialog.set_border_width (6)
  882.         dialog.set_resizable (False)
  883.         hbox = gtk.HBox (False, 12)
  884.         image = gtk.Image ()
  885.         image.set_from_stock (gtk.STOCK_DIALOG_QUESTION, gtk.ICON_SIZE_DIALOG)
  886.         image.set_alignment (0.0, 0.0)
  887.         hbox.pack_start (image, False, False, 0)
  888.         label = gtk.Label (_("Do you really want to cancel this job?"))
  889.         label.set_line_wrap (True)
  890.         label.set_alignment (0.0, 0.0)
  891.         hbox.pack_start (label, False, False, 0)
  892.         dialog.vbox.pack_start (hbox, False, False, 0)
  893.         dialog.set_data ('job-id', self.jobid)
  894.         dialog.connect ("response", self.on_job_cancel_prompt_response)
  895.         dialog.connect ("delete-event", self.on_job_cancel_prompt_delete)
  896.         dialog.show_all ()
  897.  
  898.     def on_job_cancel_prompt_delete (self, dialog, event):
  899.         self.on_job_cancel_prompt_response (dialog, gtk.RESPONSE_NO)
  900.  
  901.     def on_job_cancel_prompt_response (self, dialog, response):
  902.         jobid = dialog.get_data ('job-id')
  903.         dialog.destroy ()
  904.  
  905.         if response != gtk.RESPONSE_YES:
  906.             return
  907.  
  908.         try:
  909.             c = authconn.Connection (self.JobsWindow,
  910.                                      host=self.host,
  911.                                      port=self.port,
  912.                                      encryption=self.encryption)
  913.         except RuntimeError:
  914.             return
  915.  
  916.         c._begin_operation (_("canceling job"))
  917.         try:
  918.             c.cancelJob (jobid)
  919.         except cups.IPPError, (e, m):
  920.             if (e != cups.IPP_NOT_POSSIBLE and
  921.                 e != cups.IPP_NOT_FOUND):
  922.                 self.show_IPP_Error (e, m)
  923.             self.monitor.update ()
  924.             c._end_operation ()
  925.             return
  926.  
  927.         c._end_operation ()
  928.         del c
  929.         self.monitor.update ()
  930.  
  931.     def on_job_hold_activate(self, menuitem):
  932.         try:
  933.             c = authconn.Connection (self.JobsWindow,
  934.                                      host=self.host,
  935.                                      port=self.port,
  936.                                      encryption=self.encryption)
  937.         except RuntimeError:
  938.             return
  939.  
  940.         c._begin_operation (_("holding job"))
  941.         try:
  942.             c.setJobHoldUntil (self.jobid, "indefinite")
  943.         except cups.IPPError, (e, m):
  944.             if (e != cups.IPP_NOT_POSSIBLE and
  945.                 e != cups.IPP_NOT_FOUND):
  946.                 self.show_IPP_Error (e, m)
  947.             self.monitor.update ()
  948.             c._end_operation ()
  949.             return
  950.  
  951.         c._end_operation ()
  952.         del c
  953.         self.monitor.update ()
  954.  
  955.     def on_job_release_activate(self, menuitem):
  956.         try:
  957.             c = authconn.Connection (self.JobsWindow,
  958.                                      host=self.host,
  959.                                      port=self.port,
  960.                                      encryption=self.encryption)
  961.         except RuntimeError:
  962.             return
  963.  
  964.         c._begin_operation (_("releasing job"))
  965.         try:
  966.             c.setJobHoldUntil (self.jobid, "no-hold")
  967.         except cups.IPPError, (e, m):
  968.             if (e != cups.IPP_NOT_POSSIBLE and
  969.                 e != cups.IPP_NOT_FOUND):
  970.                 self.show_IPP_Error (e, m)
  971.             self.monitor.update ()
  972.             c._end_operation ()
  973.             return
  974.  
  975.         c._end_operation ()
  976.         del c
  977.         self.monitor.update ()
  978.  
  979.     def on_job_reprint_activate(self, menuitem):
  980.         try:
  981.             c = authconn.Connection (self.JobsWindow,
  982.                                      host=self.host,
  983.                                      port=self.port,
  984.                                      encryption=self.encryption)
  985.             c.restartJob (self.jobid)
  986.             del c
  987.         except cups.IPPError, (e, m):
  988.             self.show_IPP_Error (e, m)
  989.             self.monitor.update ()
  990.             return
  991.         except RuntimeError:
  992.             return
  993.  
  994.         self.monitor.update ()
  995.  
  996.     def on_job_authenticate_activate(self, menuitem):
  997.         self.display_auth_info_dialog (self.jobid)
  998.  
  999.     def on_refresh_activate(self, menuitem):
  1000.         self.monitor.refresh ()
  1001.  
  1002.     def job_is_active (self, jobdata):
  1003.         state = jobdata.get ('job-state', cups.IPP_JOB_CANCELED)
  1004.         if state >= cups.IPP_JOB_CANCELED:
  1005.             return False
  1006.  
  1007.         return True
  1008.  
  1009.     ## Icon manipulation
  1010.     def add_state_reason_emblem (self, pixbuf, printer=None):
  1011.         worst_reason = None
  1012.         if printer == None and self.worst_reason != None:
  1013.             # Check that it's valid.
  1014.             printer = self.worst_reason.get_printer ()
  1015.             found = False
  1016.             for reason in self.printer_state_reasons.get (printer, []):
  1017.                 if reason == self.worst_reason:
  1018.                     worst_reason = self.worst_reason
  1019.                     break
  1020.             if worst_reason == None:
  1021.                 self.worst_reason = None
  1022.  
  1023.         if printer != None:
  1024.             for reason in self.printer_state_reasons.get (printer, []):
  1025.                 if worst_reason == None:
  1026.                     worst_reason = reason
  1027.                 elif reason > worst_reason:
  1028.                     worst_reason = reason
  1029.  
  1030.         if worst_reason != None:
  1031.             level = worst_reason.get_level ()
  1032.             if level > StateReason.REPORT:
  1033.                 # Add an emblem to the icon.
  1034.                 icon = StateReason.LEVEL_ICON[level]
  1035.                 pixbuf = pixbuf.copy ()
  1036.                 theme = gtk.icon_theme_get_default ()
  1037.  
  1038.                 emblem = theme.load_icon (icon, 22, 0)
  1039.                 emblem.composite (pixbuf,
  1040.                                   pixbuf.get_width () / 2,
  1041.                                   pixbuf.get_height () / 2,
  1042.                                   emblem.get_width () / 2,
  1043.                                   emblem.get_height () / 2,
  1044.                                   pixbuf.get_width () / 2,
  1045.                                   pixbuf.get_height () / 2,
  1046.                                   0.5, 0.5,
  1047.                                   gtk.gdk.INTERP_BILINEAR, 255)
  1048.  
  1049.         return pixbuf
  1050.  
  1051.     def get_icon_pixbuf (self, have_jobs=None):
  1052.         if not self.trayicon:
  1053.             return
  1054.  
  1055.         if have_jobs == None:
  1056.             have_jobs = len (self.jobs.keys ()) > 0
  1057.  
  1058.         if have_jobs:
  1059.             pixbuf = self.icon_jobs
  1060.             for jobid, jobdata in self.jobs.iteritems ():
  1061.                 jstate = jobdata.get ('job-state', cups.IPP_JOB_PENDING)
  1062.                 if jstate == cups.IPP_JOB_PROCESSING:
  1063.                     pixbuf = self.icon_jobs_processing
  1064.                     break
  1065.         else:
  1066.             pixbuf = self.icon_no_jobs
  1067.  
  1068.         try:
  1069.             pixbuf = self.add_state_reason_emblem (pixbuf)
  1070.         except:
  1071.             nonfatalException ()
  1072.  
  1073.         return pixbuf
  1074.  
  1075.     def set_statusicon_tooltip (self, tooltip=None):
  1076.         if not self.trayicon:
  1077.             return
  1078.  
  1079.         if tooltip == None:
  1080.             num_jobs = len (self.jobs)
  1081.             if num_jobs == 0:
  1082.                 tooltip = _("No documents queued")
  1083.             elif num_jobs == 1:
  1084.                 tooltip = _("1 document queued")
  1085.             else:
  1086.                 tooltip = _("%d documents queued") % num_jobs
  1087.  
  1088.         self.statusicon.set_tooltip (tooltip)
  1089.  
  1090.     def update_status (self, have_jobs=None):
  1091.         # Found out which printer state reasons apply to our active jobs.
  1092.         upset_printers = set()
  1093.         for printer, reasons in self.printer_state_reasons.iteritems ():
  1094.             if len (reasons) > 0:
  1095.                 upset_printers.add (printer)
  1096.         debugprint ("Upset printers: %s" % upset_printers)
  1097.  
  1098.         my_upset_printers = set()
  1099.         if len (upset_printers):
  1100.             my_upset_printers = set()
  1101.             for jobid in self.active_jobs:
  1102.                 # 'job-printer-name' is set by job_added/job_event
  1103.                 printer = self.jobs[jobid]['job-printer-name']
  1104.                 if printer in upset_printers:
  1105.                     my_upset_printers.add (printer)
  1106.             debugprint ("My upset printers: %s" % my_upset_printers)
  1107.  
  1108.         my_reasons = []
  1109.         for printer in my_upset_printers:
  1110.             my_reasons.extend (self.printer_state_reasons[printer])
  1111.  
  1112.         # Find out which is the most problematic.
  1113.         self.worst_reason = None
  1114.         if len (my_reasons) > 0:
  1115.             worst_reason = my_reasons[0]
  1116.             for reason in my_reasons:
  1117.                 if reason > worst_reason:
  1118.                     worst_reason = reason
  1119.             self.worst_reason = worst_reason
  1120.             debugprint ("Worst reason: %s" % worst_reason)
  1121.  
  1122.         if self.worst_reason != None:
  1123.             (title, tooltip) = self.worst_reason.get_description ()
  1124.             if self.statusbar_set:
  1125.                 self.statusbar.pop (0)
  1126.             self.statusbar.push (0, tooltip)
  1127.             self.statusbar_set = True
  1128.         else:
  1129.             tooltip = None
  1130.             if self.statusbar_set:
  1131.                 self.statusbar.pop (0)
  1132.                 self.statusbar_set = False
  1133.  
  1134.         if self.trayicon:
  1135.             pixbuf = self.get_icon_pixbuf (have_jobs=have_jobs)
  1136.             self.set_statusicon_from_pixbuf (pixbuf)
  1137.             self.set_statusicon_visibility ()
  1138.             self.set_statusicon_tooltip (tooltip=tooltip)
  1139.  
  1140.     ## Notifications
  1141.     def notify_printer_state_reason_if_important (self, reason):
  1142.         level = reason.get_level ()
  1143.         if level < StateReason.WARNING:
  1144.             # Not important enough to justify a notification.
  1145.             return
  1146.  
  1147.         self.notify_printer_state_reason (reason)
  1148.  
  1149.     def notify_printer_state_reason (self, reason):
  1150.         tuple = reason.get_tuple ()
  1151.         if self.state_reason_notifications.has_key (tuple):
  1152.             debugprint ("Already sent notification for %s" % repr (reason))
  1153.             return
  1154.  
  1155.         level = reason.get_level ()
  1156.         if (level == StateReason.ERROR or
  1157.             reason.get_reason () == "connecting-to-device"):
  1158.             urgency = pynotify.URGENCY_NORMAL
  1159.         else:
  1160.             urgency = pynotify.URGENCY_LOW
  1161.  
  1162.         (title, text) = reason.get_description ()
  1163.         notification = pynotify.Notification (title, text, 'printer')
  1164.         reason.user_notified = True
  1165.         notification.set_urgency (urgency)
  1166.         if "actions" in pynotify.get_server_caps():
  1167.             notification.set_timeout (pynotify.EXPIRES_NEVER)
  1168.         notification.connect ('closed',
  1169.                               self.on_state_reason_notification_closed)
  1170.         self.state_reason_notifications[reason.get_tuple ()] = notification
  1171.         self.set_statusicon_visibility ()
  1172.         notification.attach_to_status_icon (self.statusicon)
  1173.         try:
  1174.             notification.show ()
  1175.         except gobject.GError:
  1176.             nonfatalException ()
  1177.  
  1178.     def on_state_reason_notification_closed (self, notification, reason=None):
  1179.         debugprint ("Notification %s closed" % repr (notification))
  1180.         notification.set_data ('closed', True)
  1181.         self.set_statusicon_visibility ()
  1182.         return
  1183.  
  1184.     ## monitor.Watcher interface
  1185.     def current_printers_and_jobs (self, mon, printers, jobs):
  1186.         monitor.Watcher.current_printers_and_jobs (self, mon, printers, jobs)
  1187.         self.set_process_pending (False)
  1188.         self.store.clear ()
  1189.         self.jobs = {}
  1190.         self.jobiters = {}
  1191.         self.printer_uri_index = PrinterURIIndex (names=printers)
  1192.         connection = None
  1193.         for jobid, jobdata in jobs.iteritems ():
  1194.             uri = jobdata.get ('job-printer-uri', '')
  1195.             try:
  1196.                 printer = self.printer_uri_index.lookup (uri,
  1197.                                                          connection=connection)
  1198.             except KeyError:
  1199.                 printer = uri
  1200.             jobdata['job-printer-name'] = printer
  1201.  
  1202.             self.add_job (jobid, jobdata, connection=connection)
  1203.  
  1204.         self.jobs = jobs
  1205.         self.active_jobs = set()
  1206.         for jobid, jobdata in jobs.iteritems ():
  1207.             if self.job_is_active (jobdata):
  1208.                 self.active_jobs.add (jobid)
  1209.  
  1210.         self.set_process_pending (True)
  1211.         self.update_status ()
  1212.  
  1213.     def job_added (self, mon, jobid, eventname, event, jobdata):
  1214.         monitor.Watcher.job_added (self, mon, jobid, eventname, event, jobdata)
  1215.  
  1216.         uri = jobdata.get ('job-printer-uri', '')
  1217.         try:
  1218.             printer = self.printer_uri_index.lookup (uri)
  1219.         except KeyError:
  1220.             printer = uri
  1221.         jobdata['job-printer-name'] = printer
  1222.  
  1223.         # We may be showing this job already, perhaps because we are showing
  1224.         # completed jobs and one was reprinted.
  1225.         if not self.jobiters.has_key (jobid):
  1226.             self.add_job (jobid, jobdata)
  1227.  
  1228.         if self.job_is_active (jobdata):
  1229.             self.active_jobs.add (jobid)
  1230.         elif jobid in self.active_jobs:
  1231.             self.active_jobs.remove (jobid)
  1232.  
  1233.         self.update_status (have_jobs=True)
  1234.         if self.trayicon:
  1235.             if not self.job_is_active (jobdata):
  1236.                 return
  1237.  
  1238.             for reason in self.printer_state_reasons.get (printer, []):
  1239.                 if not reason.user_notified:
  1240.                     self.notify_printer_state_reason_if_important (reason)
  1241.  
  1242.     def job_event (self, mon, jobid, eventname, event, jobdata):
  1243.         monitor.Watcher.job_event (self, mon, jobid, eventname, event, jobdata)
  1244.  
  1245.         uri = jobdata.get ('job-printer-uri', '')
  1246.         try:
  1247.             printer = self.printer_uri_index.lookup (uri)
  1248.         except KeyError:
  1249.             printer = uri
  1250.         jobdata['job-printer-name'] = printer
  1251.  
  1252.         if self.job_is_active (jobdata):
  1253.             self.active_jobs.add (jobid)
  1254.         elif jobid in self.active_jobs:
  1255.             self.active_jobs.remove (jobid)
  1256.  
  1257.         self.update_job (jobid, jobdata)
  1258.         self.update_status ()
  1259.         jobdata = self.jobs[jobid]
  1260.  
  1261.         # Look out for stopped jobs.
  1262.         if (self.trayicon and
  1263.             (eventname == 'job-stopped' or
  1264.              (eventname == 'job-state-changed' and
  1265.               event['job-state'] in [cups.IPP_JOB_STOPPED,
  1266.                                      cups.IPP_JOB_PENDING])) and
  1267.             not jobid in self.stopped_job_prompts):
  1268.             # Why has the job stopped?  It might be due to a job error
  1269.             # of some sort, or it might be that the backend requires
  1270.             # authentication.  If the latter, the job will be held not
  1271.             # stopped, and the job-hold-until attribute will be
  1272.             # 'auth-info-required'.  This was already checked for in
  1273.             # update_job.
  1274.             may_be_problem = True
  1275.             jstate = jobdata['job-state']
  1276.             if (jstate == cups.IPP_JOB_PROCESSING or
  1277.                 (jstate == cups.IPP_JOB_HELD and
  1278.                  jobdata['job-hold-until'] == 'auth-info-required')):
  1279.                 # update_job already dealt with this.
  1280.                 may_be_problem = False
  1281.             else:
  1282.                 # Other than that, unfortunately the only
  1283.                 # clue we get is the notify-text, which is not
  1284.                 # translated into our native language.  We'd better
  1285.                 # try parsing it.  In CUPS-1.3.6 the possible strings
  1286.                 # are:
  1287.                 #
  1288.                 # "Job stopped due to filter errors; please consult
  1289.                 # the error_log file for details."
  1290.                 #
  1291.                 # "Job stopped due to backend errors; please consult
  1292.                 # the error_log file for details."
  1293.                 #
  1294.                 # "Job held due to backend errors; please consult the
  1295.                 # error_log file for details."
  1296.                 #
  1297.                 # "Authentication is required for job %d."
  1298.                 # [This case is handled in the update_job method.]
  1299.                 #
  1300.                 # "Job stopped due to printer being paused"
  1301.                 # [This should be ignored, as the job was doing just
  1302.                 # fine until the printer was stopped for other reasons.]
  1303.                 notify_text = event['notify-text']
  1304.                 document = jobdata['job-name']
  1305.                 if notify_text.find ("backend errors") != -1:
  1306.                     message = _("There was a problem sending document `%s' "
  1307.                                 "(job %d) to the printer.") % (document, jobid)
  1308.                 elif notify_text.find ("filter errors") != -1:
  1309.                     message = _("There was a problem processing document `%s' "
  1310.                                 "(job %d).") % (document, jobid)
  1311.                 elif (notify_text.find ("being paused") != -1 or
  1312.                       jstate != cups.IPP_JOB_STOPPED):
  1313.                     may_be_problem = False
  1314.                 else:
  1315.                     # Give up and use the provided message untranslated.
  1316.                     message = _("There was a problem printing document `%s' "
  1317.                                 "(job %d): `%s'.") % (document, jobid,
  1318.                                                       notify_text)
  1319.  
  1320.             if may_be_problem:
  1321.                 debugprint ("Problem detected")
  1322.                 self.toggle_window_display (self.statusicon, force_show=True)
  1323.                 dialog = gtk.Dialog (_("Print Error"), self.JobsWindow, 0,
  1324.                                      (_("_Diagnose"), gtk.RESPONSE_NO,
  1325.                                         gtk.STOCK_OK, gtk.RESPONSE_OK))
  1326.                 dialog.set_default_response (gtk.RESPONSE_OK)
  1327.                 dialog.set_border_width (6)
  1328.                 dialog.set_resizable (False)
  1329.                 dialog.set_icon_name (ICON)
  1330.                 hbox = gtk.HBox (False, 12)
  1331.                 hbox.set_border_width (6)
  1332.                 image = gtk.Image ()
  1333.                 image.set_from_stock (gtk.STOCK_DIALOG_ERROR,
  1334.                                       gtk.ICON_SIZE_DIALOG)
  1335.                 hbox.pack_start (image, False, False, 0)
  1336.                 vbox = gtk.VBox (False, 12)
  1337.  
  1338.                 markup = ('<span weight="bold" size="larger">' +
  1339.                           _("Print Error") + '</span>\n\n' +
  1340.                           message)
  1341.                 try:
  1342.                     if event['printer-state'] == cups.IPP_PRINTER_STOPPED:
  1343.                         name = event['printer-name']
  1344.                         markup += ' '
  1345.                         markup += (_("The printer called `%s' has "
  1346.                                      "been disabled.") % name)
  1347.                 except KeyError:
  1348.                     pass
  1349.  
  1350.                 label = gtk.Label (markup)
  1351.                 label.set_use_markup (True)
  1352.                 label.set_line_wrap (True)
  1353.                 label.set_alignment (0, 0)
  1354.                 vbox.pack_start (label, False, False, 0)
  1355.                 hbox.pack_start (vbox, False, False, 0)
  1356.                 dialog.vbox.pack_start (hbox)
  1357.                 dialog.connect ('response',
  1358.                                 self.print_error_dialog_response, jobid)
  1359.                 self.stopped_job_prompts.add (jobid)
  1360.                 dialog.show_all ()
  1361.  
  1362.     def job_removed (self, mon, jobid, eventname, event):
  1363.         monitor.Watcher.job_removed (self, mon, jobid, eventname, event)
  1364.  
  1365.         if self.jobiters.has_key (jobid):
  1366.             self.store.remove (self.jobiters[jobid])
  1367.             del self.jobiters[jobid]
  1368.             del self.jobs[jobid]
  1369.  
  1370.         if jobid in self.active_jobs:
  1371.             self.active_jobs.remove (jobid)
  1372.  
  1373.         self.update_status ()
  1374.  
  1375.     def state_reason_added (self, mon, reason):
  1376.         monitor.Watcher.state_reason_added (self, mon, reason)
  1377.  
  1378.         (title, text) = reason.get_description ()
  1379.         printer = reason.get_printer ()
  1380.  
  1381.         try:
  1382.             l = self.printer_state_reasons[printer]
  1383.         except KeyError:
  1384.             l = []
  1385.             self.printer_state_reasons[printer] = l
  1386.  
  1387.         reason.user_notified = False
  1388.         l.append (reason)
  1389.         self.update_status ()
  1390.         self.treeview.queue_draw ()
  1391.  
  1392.         if not self.trayicon:
  1393.             return
  1394.  
  1395.         # Find out if the user has jobs queued for that printer.
  1396.         for job, data in self.jobs.iteritems ():
  1397.             if not self.job_is_active (data):
  1398.                 continue
  1399.             if data['job-printer-name'] == printer:
  1400.                 # Yes!  Notify them of the state reason, if necessary.
  1401.                 self.notify_printer_state_reason_if_important (reason)
  1402.                 break
  1403.  
  1404.     def state_reason_removed (self, mon, reason):
  1405.         monitor.Watcher.state_reason_removed (self, mon, reason)
  1406.  
  1407.         printer = reason.get_printer ()
  1408.         try:
  1409.             reasons = self.printer_state_reasons[printer]
  1410.         except KeyError:
  1411.             debugprint ("Printer not found")
  1412.             return
  1413.  
  1414.         try:
  1415.             i = reasons.index (reason)
  1416.         except IndexError:
  1417.             debugprint ("Reason not found")
  1418.             return
  1419.  
  1420.         del reasons[i]
  1421.  
  1422.         self.update_status ()
  1423.         self.treeview.queue_draw ()
  1424.  
  1425.         if not self.trayicon:
  1426.             return
  1427.  
  1428.         tuple = reason.get_tuple ()
  1429.         try:
  1430.             notification = self.state_reason_notifications[tuple]
  1431.             if notification.get_data ('closed') != True:
  1432.                 notification.close ()
  1433.             del self.state_reason_notifications[tuple]
  1434.             self.set_statusicon_visibility ()
  1435.         except KeyError:
  1436.             pass
  1437.  
  1438.     def still_connecting (self, mon, reason):
  1439.         monitor.Watcher.still_connecting (self, mon, reason)
  1440.         if not self.trayicon:
  1441.             return
  1442.  
  1443.         self.notify_printer_state_reason (reason)
  1444.  
  1445.     def now_connected (self, mon, printer):
  1446.         monitor.Watcher.now_connected (self, mon, printer)
  1447.  
  1448.         if not self.trayicon:
  1449.             return
  1450.  
  1451.         # Find the connecting-to-device state reason.
  1452.         try:
  1453.             reasons = self.printer_state_reasons[printer]
  1454.             reason = None
  1455.             for r in reasons:
  1456.                 if r.get_reason () == "connecting-to-device":
  1457.                     reason = r
  1458.                     break
  1459.         except KeyError:
  1460.             debugprint ("Couldn't find state reason (no reasons)!")
  1461.  
  1462.         if reason != None:
  1463.             tuple = reason.get_tuple ()
  1464.         else:
  1465.             debugprint ("Couldn't find state reason in list!")
  1466.             for (level,
  1467.                  p,
  1468.                  r) in self.state_reason_notifications.keys ():
  1469.                 if p == printer and r == "connecting-to-device":
  1470.                     debugprint ("Found from notifications list")
  1471.                     tuple = (level, p, r)
  1472.                     break
  1473.  
  1474.         try:
  1475.             notification = self.state_reason_notifications[tuple]
  1476.         except KeyError:
  1477.             debugprint ("Unexpected now_connected signal")
  1478.             return
  1479.  
  1480.         if notification.get_data ('closed') != True:
  1481.             notification.close ()
  1482.             notification.set_data ('closed', True)
  1483.  
  1484.     def printer_event (self, mon, printer, eventname, event):
  1485.         monitor.Watcher.printer_event (self, mon, printer, eventname, event)
  1486.         self.printer_uri_index.update_from_attrs (printer, event)
  1487.  
  1488.     def printer_removed (self, mon, printer):
  1489.         monitor.Watcher.printer_removed (self, mon, printer)
  1490.         self.printer_uri_index.remove_printer (printer)
  1491.  
  1492.     ### Cell data functions
  1493.     def _set_job_job_number_text (self, column, cell, model, iter, *data):
  1494.         cell.set_property("text", str (model.get_value (iter, 0)))
  1495.  
  1496.     def _set_job_user_text (self, column, cell, model, iter, *data):
  1497.         jobid = model.get_value (iter, 0)
  1498.         job = self.jobs[jobid]
  1499.         cell.set_property("text", job.get ('job-originating-user-name',
  1500.                                            _("Unknown")))
  1501.  
  1502.     def _set_job_document_text (self, column, cell, model, iter, *data):
  1503.         jobid = model.get_value (iter, 0)
  1504.         job = self.jobs[jobid]
  1505.         cell.set_property("text", job.get('job-name', _("Unknown")))
  1506.  
  1507.     def _set_job_printer_text (self, column, cell, model, iter, *data):
  1508.         jobid = model.get_value (iter, 0)
  1509.         cell.set_property("text", self.jobs[jobid]['job-printer-name'])
  1510.  
  1511.     def _set_job_size_text (self, column, cell, model, iter, *data):
  1512.         jobid = model.get_value (iter, 0)
  1513.         job = self.jobs[jobid]
  1514.         size = _("Unknown")
  1515.         if job.has_key ('job-k-octets'):
  1516.             size = str (job['job-k-octets']) + 'k'
  1517.         cell.set_property("text", size)
  1518.  
  1519.     def _find_job_state_text (self, job):
  1520.         data = self.jobs[job]
  1521.         jstate = data.get ('job-state', cups.IPP_JOB_PROCESSING)
  1522.         s = int (jstate)
  1523.         job_requires_auth = (s == cups.IPP_JOB_HELD and
  1524.                              data.get ('job-hold-until', 'none') ==
  1525.                              'auth-info-required')
  1526.         state = None
  1527.         if job_requires_auth:
  1528.             state = _("Held for authentication")
  1529.         elif s == cups.IPP_JOB_HELD:
  1530.             state = _("Held")
  1531.             until = data.get ('job-hold-until')
  1532.             if until != None:
  1533.                 try:
  1534.                     colon1 = until.find (':')
  1535.                     if colon1 != -1:
  1536.                         now = time.gmtime ()
  1537.                         hh = int (until[:colon1])
  1538.                         colon2 = until[colon1 + 1:].find (':')
  1539.                         if colon2 != -1:
  1540.                             colon2 += colon1 + 1
  1541.                             mm = int (until[colon1 + 1:colon2])
  1542.                             ss = int (until[colon2 + 1:])
  1543.                         else:
  1544.                             mm = int (until[colon1 + 1:])
  1545.                             ss = 0
  1546.  
  1547.                         day = now.tm_mday
  1548.                         if (hh < now.tm_hour or
  1549.                             (hh == now.tm_hour and
  1550.                              (mm < now.tm_min or
  1551.                               (mm == now.tm_min and ss < now.tm_sec)))):
  1552.                             day += 1
  1553.  
  1554.                         hold = (now.tm_year, now.tm_mon, day,
  1555.                                 hh, mm, ss, 0, 0, -1)
  1556.                         old_tz = os.environ.get("TZ")
  1557.                         os.environ["TZ"] = "UTC"
  1558.                         simpletime = time.mktime (hold)
  1559.  
  1560.                         if old_tz == None:
  1561.                             del os.environ["TZ"]
  1562.                         else:
  1563.                             os.environ["TZ"] = old_tz
  1564.  
  1565.                         local = time.localtime (simpletime)
  1566.                         state = _("Held until %s") % time.strftime ("%X", local)
  1567.                 except ValueError:
  1568.                     pass
  1569.             if until == "day-time":
  1570.                 state = _("Held until day-time")
  1571.             elif until == "evening":
  1572.                 state = _("Held until evening")
  1573.             elif until == "night":
  1574.                 state = _("Held until night-time")
  1575.             elif until == "second-shift":
  1576.                 state = _("Held until second shift")
  1577.             elif until == "third-shift":
  1578.                 state = _("Held until third shift")
  1579.             elif until == "weekend":
  1580.                 state = _("Held until weekend")
  1581.         else:
  1582.             try:
  1583.                 state = { cups.IPP_JOB_PENDING: _("Pending"),
  1584.                           cups.IPP_JOB_PROCESSING: _("Processing"),
  1585.                           cups.IPP_JOB_STOPPED: _("Stopped"),
  1586.                           cups.IPP_JOB_CANCELED: _("Canceled"),
  1587.                           cups.IPP_JOB_ABORTED: _("Aborted"),
  1588.                           cups.IPP_JOB_COMPLETED: _("Completed") }[s]
  1589.             except IndexError:
  1590.                 pass
  1591.  
  1592.         if state == None:
  1593.             state = _("Unknown")
  1594.  
  1595.         return state
  1596.  
  1597.     def _set_job_status_icon (self, column, cell, model, iter, *data):
  1598.         jobid = model.get_value (iter, 0)
  1599.         data = self.jobs[jobid]
  1600.         jstate = data.get ('job-state', cups.IPP_JOB_PROCESSING)
  1601.         s = int (jstate)
  1602.         if s == cups.IPP_JOB_PROCESSING:
  1603.             icon = self.icon_jobs_processing
  1604.         else:
  1605.             icon = self.icon_jobs
  1606.  
  1607.         if s == cups.IPP_JOB_HELD:
  1608.             theme = gtk.icon_theme_get_default ()
  1609.             emblem = theme.load_icon (gtk.STOCK_MEDIA_PAUSE, 22 / 2, 0)
  1610.             copy = icon.copy ()
  1611.             emblem.composite (copy, 0, 0,
  1612.                               copy.get_width (),
  1613.                               copy.get_height (),
  1614.                               copy.get_width () / 2 - 1,
  1615.                               copy.get_height () / 2 - 1,
  1616.                               1.0, 1.0,
  1617.                               gtk.gdk.INTERP_NEAREST, 255)
  1618.             icon = copy
  1619.         else:
  1620.             # Check state reasons.
  1621.             printer = data['job-printer-name']
  1622.             icon = self.add_state_reason_emblem (icon, printer=printer)
  1623.  
  1624.         cell.set_property ("pixbuf", icon)
  1625.  
  1626.     def _set_job_status_text (self, column, cell, model, iter, *data):
  1627.         jobid = model.get_value (iter, 0)
  1628.         data = self.jobs[jobid]
  1629.         try:
  1630.             text = data['_status_text']
  1631.         except KeyError:
  1632.             text = self._find_job_state_text (jobid)
  1633.             data['_status_text'] = text
  1634.  
  1635.         printer = data['job-printer-name']
  1636.         reasons = self.printer_state_reasons.get (printer, [])
  1637.         if len (reasons) > 0:
  1638.             worst_reason = reasons[0]
  1639.             for reason in reasons[1:]:
  1640.                 if reason > worst_reason:
  1641.                     worst_reason = reason
  1642.             (title, unused) = worst_reason.get_description ()
  1643.             text += " - " + title
  1644.  
  1645.         cell.set_property ("text", text)
  1646.